{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Project Solution: Goal 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For this goal we need to rewrite our sequence type `Polygons` into something that implements the iterable protocol.\n",
    "\n",
    "Furthermore, all iterators should be lazy."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll need our `Polygon` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "class Polygon:\n",
    "    def __init__(self, n, R):\n",
    "        if n < 3:\n",
    "            raise ValueError('Polygon must have at least 3 vertices.')\n",
    "        self._n = n\n",
    "        self._R = R\n",
    "        \n",
    "        self._interior_angle = None\n",
    "        self._side_length = None\n",
    "        self._apothem = None\n",
    "        self._area = None\n",
    "        self._perimeter = None\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f'Polygon(n={self._n}, R={self._R})'\n",
    "    \n",
    "    @property\n",
    "    def count_vertices(self):\n",
    "        return self._n\n",
    "    \n",
    "    @property\n",
    "    def count_edges(self):\n",
    "        return self._n\n",
    "    \n",
    "    @property\n",
    "    def circumradius(self):\n",
    "        return self._R\n",
    "    \n",
    "    @property\n",
    "    def interior_angle(self):\n",
    "        if self._interior_angle is None:\n",
    "            self._interior_angle = (self._n - 2) * 180 / self._n\n",
    "        return self._interior_angle\n",
    "\n",
    "    @property\n",
    "    def side_length(self):\n",
    "        if self._side_length is None:\n",
    "            self._side_length = 2 * self._R * math.sin(math.pi / self._n)\n",
    "        return self._side_length\n",
    "    \n",
    "    @property\n",
    "    def apothem(self):\n",
    "        if self._apothem is None:\n",
    "            self._apothem = self._R * math.cos(math.pi / self._n)\n",
    "        return self._apothem\n",
    "    \n",
    "    @property\n",
    "    def area(self):\n",
    "        if self._area is None:\n",
    "            self._area = self._n / 2 * self.side_length * self.apothem\n",
    "        return self._area\n",
    "    \n",
    "    @property\n",
    "    def perimeter(self):\n",
    "        if self._perimeter is None:\n",
    "            self._perimeter = self._n * self.side_length\n",
    "        return self._perimeter\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        if isinstance(other, self.__class__):\n",
    "            return (self.count_edges == other.count_edges \n",
    "                    and self.circumradius == other.circumradius)\n",
    "        else:\n",
    "            return NotImplemented\n",
    "        \n",
    "    def __gt__(self, other):\n",
    "        if isinstance(other, self.__class__):\n",
    "            return self.count_vertices > other.count_vertices\n",
    "        else:\n",
    "            return NotImplemented"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And here's our original implementation of `Polygons` as a sequence type:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Polygons:\n",
    "    def __init__(self, m, R):\n",
    "        if m < 3:\n",
    "            raise ValueError('m must be greater than 3')\n",
    "        self._m = m\n",
    "        self._R = R\n",
    "        self._polygons = [Polygon(i, R) for i in range(3, m+1)]\n",
    "        \n",
    "    def __len__(self):\n",
    "        return self._m - 2\n",
    "    \n",
    "    def __repr__(self):\n",
    "        return f'Polygons(m={self._m}, R={self._R})'\n",
    "    \n",
    "    def __getitem__(self, s):\n",
    "        return self._polygons[s]\n",
    "    \n",
    "    @property\n",
    "    def max_efficiency_polygon(self):\n",
    "        sorted_polygons = sorted(self._polygons, \n",
    "                                 key=lambda p: p.area/p.perimeter,\n",
    "                                reverse=True)\n",
    "        return sorted_polygons[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now need to implement the iterable protocol - which means we'll need to implement an iterator first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class PolygonsIterator:\n",
    "    def __init__(self, m, R):\n",
    "        if m < 3:\n",
    "            raise ValueError('m must be greater than 3')\n",
    "        self._m = m\n",
    "        self._R = R\n",
    "        \n",
    "    def __iter__(self):\n",
    "        return self\n",
    "    \n",
    "    def __next__(self):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is the basic skeleton we need to implement the iterator protocol.\n",
    "\n",
    "So now we need to implement the __next__ method.\n",
    "\n",
    "This method should simply return the `next` instance of a Polygon - we start with polygons with `3` sides, and work our way up.\n",
    "\n",
    "To do this, we'll use a private variable `_i` to trqack the number-of-side Polygon to hand out next. So, we'll start `_i` at `3` and work our way up."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class PolygonsIterator:\n",
    "    def __init__(self, m, R):\n",
    "        if m < 3:\n",
    "            raise ValueError('m must be greater than 3')\n",
    "        self._m = m\n",
    "        self._R = R\n",
    "        self._i = 3\n",
    "        \n",
    "    def __iter__(self):\n",
    "        return self\n",
    "    \n",
    "    def __next__(self):\n",
    "        if self._i > self._m:\n",
    "            raise StopIteration\n",
    "        else:\n",
    "            result = Polygon(self._i, self._R)\n",
    "            self._i += 1\n",
    "            return result"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's make sure the iterator works as expected:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Polygon(n=3, R=1)\n",
      "Polygon(n=4, R=1)\n",
      "Polygon(n=5, R=1)\n"
     ]
    }
   ],
   "source": [
    "p_iter = PolygonsIterator(5, 1)\n",
    "for p in p_iter:\n",
    "    print(p)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, this is an iterator, so it should be exhausted now:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "list(p_iter)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks good, so next we have to replace the sequence protocol with the iterable protocol in our `Polygons` class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Polygons:\n",
    "    def __init__(self, m, R):\n",
    "        if m < 3:\n",
    "            raise ValueError('m must be greater than 3')\n",
    "        self._m = m\n",
    "        self._R = R\n",
    "        \n",
    "    def __len__(self):\n",
    "        return self._m - 2\n",
    "    \n",
    "    def __repr__(self):\n",
    "        return f'Polygons(m={self._m}, R={self._R})'\n",
    "    \n",
    "    def __iter__(self):\n",
    "        return PolygonsIterator(self._m, self._R)\n",
    "    \n",
    "    @property\n",
    "    def max_efficiency_polygon(self):\n",
    "        sorted_polygons = sorted(self._polygons, \n",
    "                                 key=lambda p: p.area/p.perimeter,\n",
    "                                reverse=True)\n",
    "        return sorted_polygons[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now we should have an iterable:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "polygons = Polygons(5, 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Polygon(n=3, R=1)\n",
      "Polygon(n=4, R=1)\n",
      "Polygon(n=5, R=1)\n"
     ]
    }
   ],
   "source": [
    "for p in polygons:\n",
    "    print(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Polygon(n=3, R=1)\n",
      "Polygon(n=4, R=1)\n",
      "Polygon(n=5, R=1)\n"
     ]
    }
   ],
   "source": [
    "for p in polygons:\n",
    "    print(p)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, we also need to make our `max_efficiency_polygon` a lazy property:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Polygons:\n",
    "    def __init__(self, m, R):\n",
    "        if m < 3:\n",
    "            raise ValueError('m must be greater than 3')\n",
    "        self._m = m\n",
    "        self._R = R\n",
    "        self._max_efficiency_polygon = None\n",
    "        \n",
    "    def __len__(self):\n",
    "        return self._m - 2\n",
    "    \n",
    "    def __repr__(self):\n",
    "        return f'Polygons(m={self._m}, R={self._R})'\n",
    "    \n",
    "    def __iter__(self):\n",
    "        return PolygonsIterator(self._m, self._R)\n",
    "    \n",
    "    @property\n",
    "    def max_efficiency_polygon(self):\n",
    "        if self._max_efficiency_polygon is None:\n",
    "            sorted_polygons = sorted(self._polygons, \n",
    "                                     key=lambda p: p.area/p.perimeter,\n",
    "                                    reverse=True)\n",
    "            self._max_efficiency_polygon = sorted_polygons[0]\n",
    "        return self._max_efficiency_polygon"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's test that to make sure it still calculates correctly (should always return the largest (in terms of edges/vertices) Polygon in the iterable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Polygons' object has no attribute '_polygons'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-12-a69efa15bd2e>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[0;32m      1\u001b[0m \u001b[0mpolygons\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mPolygons\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m10\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mpolygons\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmax_efficiency_polygon\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;32m<ipython-input-11-69e2f4e6f9be>\u001b[0m in \u001b[0;36mmax_efficiency_polygon\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m     19\u001b[0m     \u001b[1;32mdef\u001b[0m \u001b[0mmax_efficiency_polygon\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     20\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_max_efficiency_polygon\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 21\u001b[1;33m             sorted_polygons = sorted(self._polygons, \n\u001b[0m\u001b[0;32m     22\u001b[0m                                      \u001b[0mkey\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mlambda\u001b[0m \u001b[0mp\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0marea\u001b[0m\u001b[1;33m/\u001b[0m\u001b[0mp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mperimeter\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     23\u001b[0m                                     reverse=True)\n",
      "\u001b[1;31mAttributeError\u001b[0m: 'Polygons' object has no attribute '_polygons'"
     ]
    }
   ],
   "source": [
    "polygons = Polygons(10, 1)\n",
    "print(polygons.max_efficiency_polygon)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, we have a slight problem. We also need to change the iterable passed to the `sorted` method - we no longer have a list of Polygons.\n",
    "\n",
    "But that's easily fixed since `sorted` can work with iterables and iterators in general!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Polygons:\n",
    "    def __init__(self, m, R):\n",
    "        if m < 3:\n",
    "            raise ValueError('m must be greater than 3')\n",
    "        self._m = m\n",
    "        self._R = R\n",
    "        self._max_efficiency_polygon = None\n",
    "        \n",
    "    def __len__(self):\n",
    "        return self._m - 2\n",
    "    \n",
    "    def __repr__(self):\n",
    "        return f'Polygons(m={self._m}, R={self._R})'\n",
    "    \n",
    "    def __iter__(self):\n",
    "        return PolygonsIterator(self._m, self._R)\n",
    "    \n",
    "    @property\n",
    "    def max_efficiency_polygon(self):\n",
    "        if self._max_efficiency_polygon is None:\n",
    "            sorted_polygons = sorted(PolygonsIterator(self._m, self._R), \n",
    "                                     key=lambda p: p.area/p.perimeter,\n",
    "                                    reverse=True)\n",
    "            self._max_efficiency_polygon = sorted_polygons[0]\n",
    "        return self._max_efficiency_polygon"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And let's test that again:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Polygon(n=10, R=1)\n"
     ]
    }
   ],
   "source": [
    "polygons = Polygons(10, 1)\n",
    "print(polygons.max_efficiency_polygon)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, that seems to work!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
